﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.Linq;
using System.IO;

namespace Admx_Generator
{
    public class AdmxFile
    {
        private List<Category> _categories = new List<Category>();
        private List<Policy> _policies = new List<Policy>();
        private List<Namespace> _usedNamespaces = new List<Namespace>();
        private Namespace _targetedNamespace = null;
        private string _supersededAdm = String.Empty;
        private SupportedOnItem _supportedOnItem = null;
        private Revision _revision = new Revision(1, 0);
        private Resources _resources = new Resources(new AdmlFile("en-US"), new Revision(1, 0));

        /// <summary>
        /// Create a new instance of an AdmxFile from scratch
        /// </summary>
        /// <param name="filename">Filename of the future AdmxFile.</param>
        /// <param name="admxNamespace">Namespace targeted by this AdmxFile.</param>
        /// <param name="fallbackCulture">Fallback culture of this AdmxFile.</param>
        public AdmxFile(string filename, Namespace admxNamespace, culture fallbackCulture)
        {
            this.Filename = filename;
            this._targetedNamespace = admxNamespace;
            this._resources = new Resources(new AdmlFile(fallbackCulture.CultureInfo.Name), new Revision(1, 0));
        }

        /// <summary>
        /// Create an instance of an AdmxFile from an existing Admx file.
        /// </summary>
        /// <param name="filename">Admx file to read to create this instance.</param>
        public AdmxFile(string filename)
        {
            XDocument xmlDoc = XDocument.Load(filename);

            XElement policyDefinitions = xmlDoc.Root;
            XNamespace ns = policyDefinitions.GetDefaultNamespace();
            this.Revision = new Revision(policyDefinitions.Attribute("revision").Value);
            XElement policyNamespaces = policyDefinitions.Element(ns + "policyNamespaces");
            XElement supersededAdm = policyDefinitions.Element(ns + "supersededAdm");
            XElement resources = policyDefinitions.Element(ns + "resources");
            XElement supportedOn = policyDefinitions.Element(ns + "supportedOn");
            XElement categories = policyDefinitions.Element(ns + "categories");
            XElement policies = policyDefinitions.Element(ns + "policies");

            // policynamespaces
            XElement target = policyNamespaces.Element(ns + "target");
            List<XElement> usings = policyNamespaces.Elements(ns + "using").ToList<XElement>();
            this.TargetedNamespace = new Namespace(target.Attribute("namespace").Value, target.Attribute("prefix").Value);
            foreach (XElement usingElement in usings)
            {
                this.UsedNamespaces.Add(new Namespace(usingElement.Attribute("namespace").Value, usingElement.Attribute("prefix").Value));
            }

            // supersededAdm
            if (supersededAdm != null)
            {
                this.SupersededAdm = supersededAdm.Attribute("fileName").Value;
            }

            // resources
            XAttribute fallbackCulture = resources.Attribute("fallbackCulture");
            XAttribute minRequiredRevision = resources.Attribute("minRequiredRevision");
            this.Resources = new Resources(fallbackCulture != null ? new AdmlFile(fallbackCulture.Value) : new AdmlFile("en-us"), new Revision(minRequiredRevision.Value));

            // supportedOn


            // categories
            List<XElement> allCategories = categories.Elements(ns + "category").ToList<XElement>();
            foreach (XElement category in allCategories)
            {
                XElement parentCategory = category.Element(ns + "parentCategory");
                XAttribute parentRef = parentCategory != null ? parentCategory.Attribute("ref") : null;
                XAttribute name = category.Attribute("name");
                XAttribute displayName = category.Attribute("displayName");
                XAttribute explainText = category.Attribute("explainText");

                Category newCategory = new Category(name.Value, displayName.Value, explainText == null? null: explainText.Value, parentRef ==null ? null : new Reference(parentRef.Value));
                this.AddCategory(newCategory);
            }
            // policies
            List<XElement> allPolicies = policies.Elements(ns + "policy").ToList<XElement>();
            foreach (XElement policy in allPolicies)
            {
                XElement policyParentCategory = policy.Element(ns + "parentCategory");
                XElement policySupportedOn = policy.Element(ns + "supportedOn");
                XElement enabledValue = policy.Element(ns + "enabledValue");
                XElement disabledValue = policy.Element(ns + "disabledValue");
                XElement enabledList = policy.Element(ns + "enabledList");
                XElement disabledList = policy.Element(ns + "disabledList");
                XElement elements = policy.Element(ns + "elements");
                XAttribute parentCategoryRef = policyParentCategory.Attribute("ref");
                XAttribute policySupportedOnRef = policySupportedOn != null ? policySupportedOn.Attribute("ref") : null;
                XAttribute name = policy.Attribute("name");
                XAttribute policyClass = policy.Attribute("class");
                XAttribute displayName = policy.Attribute("displayName");
                XAttribute explainText = policy.Attribute("explainText");
                XAttribute presentation = policy.Attribute("presentation");
                XAttribute key = policy.Attribute("key");
                XAttribute valueName = policy.Attribute("valueName");

                Policy newPolicy = new Policy(name.Value,
                    (Policy.PolicyClass)Enum.Parse(typeof(Policy.PolicyClass), policyClass.Value),
                    displayName.Value,
                    explainText != null ? explainText.Value : null,
                    presentation != null ? presentation.Value : null,
                    key.Value,
                    valueName != null ? valueName.Value : null,
                    new Reference(parentCategoryRef.Value),
                    policySupportedOnRef != null ? new Reference(policySupportedOnRef.Value) : null);
                this.AddPolicy(newPolicy);
            }

            xmlDoc = null;
        }

        #region (Internal Properties)

        /// <summary>
        /// Gets or Sets the name of the ADMX file.
        /// </summary>
        public string Filename { get; private set; }

        public Revision Revision
        {
            get { return this._revision; }
            set
            {
                if (!this._revision.Equals(value))
                {
                    this._revision = value;
                    if (this.OnChanged != null)
                        this.OnChanged(this);
                }
            }
        }

        public Resources Resources
        {
            get { return this._resources; }
            set
            {
                if (!this._resources.Equals(value))
                {
                    this._resources = value;
                    if (this.OnChanged != null)
                        this.OnChanged(this);
                }
            }
        }

        /// <summary>
        /// Gets or Sets the namespace targeted by this Group Policy Template.
        /// </summary>
        public Namespace TargetedNamespace
        {
            get { return this._targetedNamespace; }

            set
            {
                if (this._targetedNamespace == null || !this._targetedNamespace.Equals(value))
                {
                    this._targetedNamespace = value;
                    if (this.OnChanged != null)
                        this.OnChanged(this);
                }
            }
        }

        /// <summary>
        /// Get a list of Namespace used by this AdmxFile. Use the 'AddUsedNamespace' to add Namespace to this list.
        /// </summary>
        public List<Namespace> UsedNamespaces
        {
            get { return this._usedNamespaces; }
            set
            {
                if (!this._usedNamespaces.Equals(value))
                {
                    this._usedNamespaces = value;
                    if (this.OnChanged != null)
                        this.OnChanged(this);
                }
            }
        }

        /// <summary>
        /// Gets or Sets the name of the superseded Adm file
        /// </summary>
        public string SupersededAdm
        {
            get { return this._supersededAdm; }
            set
            {
                if (value != null && this._supersededAdm != value)
                {
                    this._supersededAdm = value;
                    if (this.OnChanged != null)
                        this.OnChanged(this);
                }
            }
        }

        /// <summary>
        /// Gets or sets the SupportedOn item to include in this Group Policy definition.
        /// </summary>
        public SupportedOnItem SupportedOn
        {
            get { return this._supportedOnItem; }
            set
            {
                if (this._supportedOnItem == null || !this._supportedOnItem.Equals(value))
                {
                    this.AddUsedNamespace(value.Namespace);
                    this._supportedOnItem = value;
                    if (this.OnChanged != null)
                        this.OnChanged(this);
                }
            }
        }

        /// <summary>
        /// Get the list of all Categories embedded in this Group Policy Template. Use the 'AddCategory' to add item to this list.
        /// </summary>
        public System.Collections.ObjectModel.ReadOnlyCollection<Category> Categories { get { return new System.Collections.ObjectModel.ReadOnlyCollection<Category>(this._categories); } }

        /// <summary>
        /// Get the list of all policies in this Group Policy Template. to Add or Remove policy, use 'AddPolicy' and 'RemovePolicy' methods.
        /// </summary>
        public System.Collections.ObjectModel.ReadOnlyCollection<Policy> Policies { get { return new System.Collections.ObjectModel.ReadOnlyCollection<Policy>(this._policies); } }

        #endregion (Internal Properties)

        #region (internal methods)

        /// <summary>
        /// Rename this Admx
        /// </summary>
        /// <param name="newName">New name of this Admx</param>
        public void Rename(string newName)
        {
            if (String.Compare(this.Filename, newName, StringComparison.InvariantCultureIgnoreCase) != 0)
            {
                this.Filename = newName;
                if (this.OnChanged != null)
                    this.OnChanged(this);
            }
        }

        /// <summary>
        /// Add a namespace in the 'using' Element.
        /// </summary>
        /// <param name="namespaceToAdd">Namespace to add.</param>
        public void AddUsedNamespace(Namespace namespaceToAdd)
        {
            if (!this.UsedNamespaces.Contains(namespaceToAdd))
            {
                this.UsedNamespaces.Add(namespaceToAdd);
                if (this.OnChanged != null)
                    this.OnChanged(this);
            }
            else
                throw new Admx_Generator.Exceptions.DuplicateNamespaceException(namespaceToAdd);
        }

        /// <summary>
        /// Add a Category to the list of Categories of this Group Policy Template.
        /// </summary>
        /// <param name="categoryToAdd">Category to add.</param>
        public void AddCategory(Category categoryToAdd)
        {
            if (!this._categories.Contains(categoryToAdd))
            {
                this._categories.Add(categoryToAdd);
                if (this.OnChanged != null)
                    this.OnChanged(this);
            }
            else
                throw new Admx_Generator.Exceptions.DuplicateCategoryException(categoryToAdd);
        }

        /// <summary>
        /// Remove the Category from the list.
        /// </summary>
        public void RemoveCategory(Category categoryToRemove)
        {
            if (this.CategoryExists(categoryToRemove))
            {
                this._categories.Remove(categoryToRemove);
                if (this.OnChanged != null)
                    this.OnChanged(this);
            }
        }

        /// <summary>
        /// Check whether or not, if the provided category exists in this Admx file.
        /// </summary>
        /// <param name="category">Category to search.</param>
        /// <returns>True if the Category exists.</returns>
        public bool CategoryExists(Category category)
        {
            return this.Categories.Contains(category);
        }

        /// <summary>
        /// Remove the UsedNamespace.
        /// </summary>
        /// <param name="namespacetoRemove">The namespace to remove.</param>
        public void RemoveUsedNamespace(Namespace namespacetoRemove)
        {
            if (this.UsedNamespaces.Contains(namespacetoRemove))
            {
                this.UsedNamespaces.Remove(namespacetoRemove);
                if (this.OnChanged != null)
                    this.OnChanged(this);
            }
        }

        /// <summary>
        /// Add the policyToRemove to the list.
        /// </summary>
        /// <param name="policyToAdd">Policy to add.</param>
        public void AddPolicy(Policy policyToAdd)
        {
            bool found = false;

            foreach (Policy policy in this.Policies)
            {
                if (policy.Name == policyToAdd.Name)
                {
                    found = true;
                    break;
                }
            }
            if (!found)
            {
                this._policies.Add(policyToAdd);
                if (this.OnChanged != null)
                    this.OnChanged(this);
            }
        }

        /// <summary>
        /// Remove the policyToRemove from the list
        /// </summary>
        /// <param name="policyToRemove">Policy to remove.</param>
        public void RemovePolicy(Policy policyToRemove)
        {
            if (this.Policies.Contains(policyToRemove))
            {
                this._policies.Remove(policyToRemove);
                if (this.OnChanged != null)
                    this.OnChanged(this);
            }
        }

        /// <summary>
        /// Save this Admx File to the disk.
        /// </summary>
        /// <param name="fullPath">Full path where to save the ADMX file</param>
        internal void Save(string fullPath)
        {
            XmlWriterSettings writerSettings = new XmlWriterSettings();
            writerSettings.Encoding = Encoding.UTF8;
            writerSettings.Indent = true;
            FileInfo admxFile = new FileInfo(Path.Combine(fullPath, this.Filename + ".admx"));
            StreamWriter writer = admxFile.CreateText();

            XmlWriter xmlWriter = XmlWriter.Create(writer, writerSettings);

            xmlWriter.WriteStartDocument();

            // policyDefinitions
            xmlWriter.WriteStartElement("policyDefinitions", @"http://schemas.microsoft.com/GroupPolicy/2006/07/PolicyDefinitions");
            xmlWriter.WriteAttributeString("xsd", XNamespace.Xmlns.ToString(), @"http://www.w3.org/2001/XMLSchema");
            xmlWriter.WriteAttributeString("xsi", XNamespace.Xmlns.ToString(), @"http://www.w3.org/2001/XmlSchema-instance");
            xmlWriter.WriteAttributeString("revision", this.Revision.ToString());
            xmlWriter.WriteAttributeString("schemaVersion", "1.0");
            // policyNamespaces
            xmlWriter.WriteStartElement("policyNamespaces");
            // target
            xmlWriter.WriteStartElement("target");
            xmlWriter.WriteAttributeString("prefix", this.TargetedNamespace.Prefix);
            xmlWriter.WriteAttributeString("namespace", this.TargetedNamespace.Target);

            xmlWriter.WriteEndElement(); // target

            foreach (Namespace usingElement in this.UsedNamespaces)
            {
                // using
                xmlWriter.WriteStartElement("using");
                xmlWriter.WriteAttributeString("prefix", usingElement.Prefix);
                xmlWriter.WriteAttributeString("namespace", usingElement.Target);
                xmlWriter.WriteEndElement(); // using
            }

            xmlWriter.WriteEndElement(); // policyNamespaces

            // supersededAdm
            if (!String.IsNullOrEmpty(this.SupersededAdm))
            {
                xmlWriter.WriteStartElement("supersededAdm");
                if (!this.SupersededAdm.EndsWith(".adm", StringComparison.InvariantCultureIgnoreCase))
                { this.SupersededAdm += ".adm"; }
                xmlWriter.WriteAttributeString("fileName", this.SupersededAdm);

                xmlWriter.WriteEndElement(); // supersededAdm
            }

            // resources
            xmlWriter.WriteStartElement("resources");
            xmlWriter.WriteAttributeString("fallbackCulture", this.Resources.FallbackCulture.Culture.Name);
            xmlWriter.WriteAttributeString("minRequiredRevision", this.Resources.MinRequiredRevision.ToString());

            xmlWriter.WriteEndElement(); // resources

            // supportedOn
            if (this.SupportedOn != null)
            {
                xmlWriter.WriteStartElement("supportedOn");
                xmlWriter.WriteAttributeString("ref", this.SupportedOn.Namespace.Prefix + ":" + this.SupportedOn.NameAttribute);

                xmlWriter.WriteEndElement(); // supportedOn
            }

            if (this.Categories.Count != 0)
            {
                // categories
                xmlWriter.WriteStartElement("categories");

                foreach (Category category in this.Categories)
                {
                    if (category.ParentCategory == null || category.ParentCategory.NamespacePrefix == String.Empty)
                    {
                        // category
                        xmlWriter.WriteStartElement("category");
                        xmlWriter.WriteAttributeString("name", category.Name);
                        xmlWriter.WriteAttributeString("displayName", category.DisplayName);
                        if (!String.IsNullOrEmpty(category.ExplainText))
                        {
                            xmlWriter.WriteAttributeString("explainText", category.ExplainText);
                        }
                        if (category.ParentCategory != null)
                        {
                            // parentCategory
                            xmlWriter.WriteStartElement("parentCategory");
                            string attributeValue = string.Empty;
                            attributeValue = category.ParentCategory.ToString();
                            xmlWriter.WriteAttributeString("ref", attributeValue);
                            xmlWriter.WriteEndElement(); // parentCategory
                        }
                        xmlWriter.WriteEndElement(); // category
                    }
                }
                xmlWriter.WriteEndElement(); // categories
            }

            xmlWriter.WriteEndElement(); // policyDefinitions
            xmlWriter.WriteEndDocument();
            xmlWriter.Flush();
            xmlWriter.Close();
            writer.Flush();
            writer.Close();
        }

        #endregion (internal methods)

        #region (public events)

        public delegate void OnChangeEventHandler(AdmxFile sender);

        /// <summary>
        /// This event is fire each time a property is changed.
        /// </summary>
        internal event OnChangeEventHandler OnChanged;

        #endregion (public events)

    }
}
